#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <stdio.h>
#include <ctype.h>

#include "debug.h"
#include "ini.h"

typedef struct ini_section ini_section_t;
typedef struct ini_value ini_value_t;
typedef struct ini_node ini_node_t;

/* some TO-DOs:
     add some error enums and error strings at (all of?) the "TRACE" calls.
*/

struct ini_section {
    char * name;
    ini_value_t * data;
    ini_section_t * left, * right;
};

struct ini_value {
    char * name;
    void * data;
    int type;
    ini_value_t * left, * right;
};

struct ini_node {
    uid_t uid;
    ini_section_t * data;
    ini_node_t * left, * right;
};

char * errorstr = NULL; // string containing error string
int errorID = INI_ERROR_NONE; // last error number/ID

ini_node_t * loaded = NULL; // root of ini file tree.
ini_node_t * selected = NULL; // the ini file currently selected

ini_value_t *fetchValue(ini_section_t *section, const char *value) {
    // can't work with an invalid pointer
    unsigned int val_length = strlen(value);
    char *name = malloc(val_length + 1);
    memset(name, 0, val_length + 1);
    memcpy(name, value, val_length);
    for(; val_length > 0; -- val_length) {
        name[val_length-1] = tolower(value[val_length-1]);
    }
    if(!section) {
        TRACE("Cannot fetch value from NULL section!\n");
        return NULL;
    }
    // navigate the value tree until the right name is found
    ini_value_t *traverse = section->data;
    while(traverse) {
        // check to see if this node IS the value we want
        int result = strcmp(name, traverse->name);
        if(result == 0)
            return traverse;
        // otherwise, traverse the tree
        if(result > 0) {
            // if there is no right node, the value isn't in the tree
            if(!traverse->right) return NULL;
            // go right
            traverse = traverse->right;
        }
        else {
            // if there is no left node, the value isn't in the tree
            if(!traverse->left) return NULL;
            // go left
            traverse = traverse->left;
        }
    }
    // if we got here, then we haven't found our result
    TRACE("Value '%s' not found in section '%s')!\n", name, section->name);
    return NULL;
}

ini_section_t *fetchSection(ini_node_t *node, const char *section) {
    // can't work with an invalid pointer
    if(!node) {
        TRACE("Cannot fetch section from NULL node!\n");
        return NULL;
    }
    // navigate the section tree until the right name is found
    ini_section_t *traverse = node->data;
    while(traverse) {
        // check to see if this node IS the section we want
        int result = strcmp(section, traverse->name);
        if(result == 0)
            return traverse;
        // otherwise, traverse the tree
        if(result > 0) {
            // if there is no right node, the value isn't in the tree
            if(!traverse->right) return NULL;
            // go right
            traverse = traverse->right;
        }
        else {
            // if there is no left node, the value isn't in the tree
            if(!traverse->left) return NULL;
            // go left
            traverse = traverse->left;
        }
    }
    // if we got here, then we haven't found our result
    TRACE("Section '%s' not found in INI file (UID: %i)!\n", section, node->uid);
    return NULL;
}

ini_value_t *fetchValueStr(const char * section, const char * value) {
    // if nothing is selected, we can't get anything
    if(!selected) {
        TRACE("Cannot get value - no INI file selected!\n");
        return NULL;
    }
    // fetch the section specified
    ini_section_t * sect = fetchSection(selected, section);
    // if null, return void, woopsie
    if(!sect) {
        TRACE("Invalid section specified!\n");
        return NULL;
    }
    ini_value_t * val = fetchValue(sect, value);
    if(!val) {
        TRACE("Invalid value name specified!\n");
        return NULL;
    }
    // return what we found
    return val;
}

ini_node_t * fetchNode(uid_t uid) {
    // if there is no tree, we can't fetch anything
    if(!loaded) {
        return NULL;
    }
    // iterate the nodes until we reach the end or we find the right UID
    ini_node_t *start = loaded;
    while(start && start->uid != uid) {
        if(uid > start->uid)
            start = start->right;
        else
            start = start->left;
    }
    // if start is NULL, we reached the end and the UID was not found
    if(!start) {
        TRACE("UID not present within ini node tree!\n");
        return NULL;
    }
    // start isn't NULL, so we have the UID we wanted
    return start;
}

ini_node_t * fetchParentNode(uid_t uid) {
    // if there is no tree we can't fetch anything
    if(!loaded) {
        TRACE("Cannot fetch ini parent node - tree uninitialised!\n");
        return NULL;
    }
    // check the root node isn't the node we want
    if(loaded->uid == uid) {
        // awkward to deal with - will return NULL for now... best solution?
        return NULL;
    }
    // iterate the nodes until we reach the end or we find the parent of the node with UID == uid
    ini_node_t *start = loaded;
    while(start) {
        if(uid > start->uid) {
            if(start->right && start->right->uid == uid)
                return start;
            else
                start = start->right;
        }
        else {
            if(start->left && start->left->uid == uid)
                return start;
            else
                start = start->left;
        }
    }
    // if we get here the node wasn't found
    TRACE("UID not present in node tree, therefore neither is parent node!\n");
    return NULL;
}

void addNode(ini_node_t *node) {
    // if there is no tree, then this is the new root
    if(!loaded) {
        loaded = node;
        return;
    }
    // traverse the tree and place it where it belongs, binary tree blahblahblah
    ini_node_t *traverse = loaded;
    while(traverse) {
        printf("looking at %i\n", traverse->uid);
        if(node->uid > traverse->uid) {
            // if there is no right node, add there, else check the right node
            if(!traverse->right) {
                traverse->right = node;
                return;
            }
            else
                traverse = traverse->right;
        }
        else {
            // if there is no left node, add there, else check the left node
            if(!traverse->left) {
                traverse->left = node;
                return;
            }
            else
                traverse = traverse->left;
        }
    }
    // if we're here, we clearly didn't add it to the tree... which is worrying. (Is it even possible?)
    TRACE("Unable to add new node to INI node tree!\n");
}

void iniSelect(uid_t uid) {
    // find the node specified, if it's present
    ini_node_t * node = fetchNode(uid);
    // if it is, point at it with 'selected' and return
    if(node) {
        selected = node;
        return;
    }
    // we have to load the node and add it to the tree
    ini_node_t * add = malloc(sizeof(*add));
    if(!add) {
        TRACE("OUT OF MEMORY!\n");
        return;
    }
    // fill it with any data we can at the moment
    add->uid = uid;
    add->data = NULL;
    add->left = NULL;
    add->right = NULL;
    // add it to the tree
    addNode(add);
    // point to the node with selected
    selected = add;
}

void freeValue(ini_value_t * value) {
    if(value->left)
        freeValue(value->left);
    if(value->right)
        freeValue(value->right);
    free(value->data);
    free(value->name);
    free(value);
}

void freeSection(ini_section_t * section) {
    if(section->left)
        freeSection(section->left);
    if(section->right)
        freeSection(section->right);
    if(section->data)
        freeValue(section->data);
    free(section->name);
    free(section);
}

void freeNode(ini_node_t * node) {
    if(node->data)
        freeSection(node->data);
    free(node);
}

void iniDispose() {
    // if we haven't got anything selected, we can't dispose of it
    if(!selected) {
        TRACE("No node selected to dispose of!\n");
        return;
    }
    // store these while we delete one
    ini_node_t * left, * right, * parent;
    left = selected->left;
    right = selected->right;
    // tell the parent node that one of it's children is deleted
    parent = fetchParentNode(selected->uid);
    if(parent) {
        if(parent->left == selected)
            parent->left = NULL;
        if(parent->right == selected)
            parent->right = NULL;
    }
    else if(selected == loaded) {
        loaded = NULL;
    }
    // free all memory within the selected node
    freeNode(selected);
    selected = NULL;
    // the left and right node are no longer pointed to, so add them to the tree again
    if(left)
        addNode(left);
    if(right)
        addNode(right);
}

void iniParseFile(const char *filename) {
    // if nothing is currently selected, we can't place what we load anywhere
    if(!selected) {
        TRACE("Unable to parse INI file '%s' - no UID selected!\n", filename);
        return;
    }
    // open the file
    FILE *handle = fopen(filename, "r");
    // error handling *yawn*
    if(!handle) {
        TRACE("Unable to open INI file '%s'!\n", filename);
        return;
    }
    // get the size of the INI file in bytes
    if(fseek(handle, 0, SEEK_END) != 0) {
        TRACE("Unable to seek to the end of INI file '%s'!\n", filename);
        return;
    }
    long filesize = ftell(handle);
    if(filesize == -1) {
        TRACE("Unable to retrieve file position for INI file '%s'!\n", filename);
        return;
    }
    // allocate enough memory to store the data in the file (+1 for 0 delimiter)
    char *contents = malloc(filesize + 1);
    // clear it for epic cleanliness
    memset(contents, 0, filesize+1);
    if(!contents) {
        TRACE("OUT OF MEMORY!\n");
        return;
    }
    // point back to the start of the file for reading
    rewind(handle);
    // read into buffer (it's just text, so I put '1' as element size?)
    fread(contents, 1, filesize, handle);
    // parse the file contents
    iniParseLump(contents);
    // free buffer (DIDN'T FORGET THIS HONEST <_< >_>)
    free(contents);
}

int rfind(const char *str, char chr, int offset) {
    while(offset >= 0 && str[offset] != chr) -- offset;
    return offset;
}

void print_some(const char *str, unsigned int length) {
    while(length > 0) {
        putc(*str, stdout);
        ++str;
        --length;
    }
}

ini_section_t * addSection(const char *name, unsigned int length) {
    // we can't add sections to what doesn't exist
    if(!selected) {
        TRACE("Cannot add section - no node selected!\n");
        return NULL;
    }
    // create the new section node
    ini_section_t * node = malloc(sizeof(*node));
    node->left = NULL;
    node->right = NULL;
    node->data = NULL;
    node->name = malloc(length + 1);
    memset(node->name, 0, length + 1);
    memcpy(node->name, name, length);
    // traverse the tree and place it where it belongs
    ini_section_t *traverse = selected->data;
    // if there is no section node yet, then this is the root
    if(!traverse) {
        selected->data = node;
        return node;
    }
    while(traverse) {
        int comparison = strcmp(node->name, traverse->name);
        // this node already exists if 0
        if(comparison == 0) {
            freeSection(node);
            return traverse; // it's already added
        }
        if(comparison > 0) {
            // if there is no right node, add there, else check the right node
            if(!traverse->right) {
                traverse->right = node;
                return node;
            }
            else
                traverse = traverse->right;
        }
        else {
            // if there is no left node, add there, else check the left node
            if(!traverse->left) {
                traverse->left = node;
                return node;
            }
            else
                traverse = traverse->left;
        }
    }
    // if we're here, we clearly didn't add it to the tree... which is worrying. (Is it even possible?)
    TRACE("Unable to add new node to INI section tree!\n");
    freeSection(node);
    return NULL;
}

int parseSectionHeader(const char *header, unsigned int length, ini_section_t **current) {
    // if there isn't a '[' we shouldn't be here
    if(header[0] != '[') {
        TRACE("Error parsing INI section header!\n");
        errorID = INI_ERROR_PARSE_ERROR;
        errorstr = "INI section header must start with '[' symbol";
        return -1;
    }
    // find the ']'
    unsigned int offset = 0;
    while(header[offset] && header[offset] != ']') ++ offset;
    // if we didn't find a ']' then error
    if(!header[offset]) {
        TRACE("Error parsing INI section header!\n");
        errorID = INI_ERROR_PARSE_ERROR;
        errorstr = "INI section header must start with ']' symbol";
        return -1;
    }
    // the header name is character 1 to offset-1
    ini_section_t * add = addSection(header+1, offset-1);
    if(!add) {
        TRACE("Unable to add section to INI section tree (UID: %i)!\n", selected->uid);
        return -1;
    }
    // we've added the section to the tree, so now set it as the current section
    *current = add;
    // voila?
    return 0;
}

int addValue(ini_section_t * section, const char *name, unsigned int length, void * value, int type) {
    ini_value_t * add = malloc(sizeof(*add));
    add->left = NULL;
    add->right = NULL;
    add->name = malloc(length+1);
    memset(add->name, 0, length+1);
    memcpy(add->name, name, length);
    char *ptr = add->name;
    for(; *ptr; ++ptr) *ptr = tolower(*ptr);
    add->data = value;
    add->type = type;
    ini_value_t * traverse = section->data; // the root value of the section node we want to work with
    if(!traverse) {
        // the tree is empty, just make this the root and exit success
        section->data = add;
        return 0;
    }
    while(traverse) {
        int comparison = strcmp(add->name, traverse->name);
        // this node already exists if 0
        if(comparison == 0) {
            free(traverse->data);
            traverse->data = add->data;
            traverse->type = add->type;
            add->data = NULL;
            freeValue(add);
            return 0; // it's already added
        }
        if(comparison > 0) {
            // if there is no right node, add there, else check the right node
            if(!traverse->right) {
                traverse->right = add;
                return 0;
            }
            else
                traverse = traverse->right;
        }
        else {
            // if there is no left node, add there, else check the left node
            if(!traverse->left) {
                traverse->left = add;
                return 0;
            }
            else
                traverse = traverse->left;
        }
    }
    // if we get here presumably it wasn't added to the tree
    // free 'add' and error out
    TRACE("Failed to add new value node to tree (UID: %i, section: %s)!\n", selected->uid, section->name);
    freeValue(add);
    return -1;
}

int parseValue(const char * value, unsigned int length, void ** data) {
    // parse as a string
    if(value[0] == '"') {
        if(value[length-1] != '"') {
            TRACE("Invalid string value in INI file, no terminating '\"' character!\n");
            return INI_TYPE_VOID;
        }
        *data = malloc(length-1);
        memset(*data, 0, length-1);
        memcpy(*data, value+1, length-2);
        return INI_TYPE_STRING;
    }
    // parse as boolean
    else if(isalpha(value[0])) {
        int boolean = 0;
        if(length == 5  && tolower(value[0]) == 'f'
                        && tolower(value[1]) == 'a'
                        && tolower(value[2]) == 'l'
                        && tolower(value[3]) == 's'
                        && tolower(value[4]) == 'e')
            boolean = 0;
        else if(length == 4 && tolower(value[0]) == 't'
                            && tolower(value[1]) == 'r'
                            && tolower(value[2]) == 'u'
                            && tolower(value[3]) == 'e')
            boolean = 1;
        else {
            TRACE("Unable to parse boolean value in INI file!\n");
            return INI_TYPE_VOID;
        }
        *data = malloc(sizeof(boolean));
        *(*(int**)data) = boolean;
        return INI_TYPE_BOOLEAN;
    }
    // parse as integer/double
    else if(isdigit(value[0])) {
        if(rfind(value, '.', length) == -1) {
            // parse as integer
            long val = strtol(value, NULL, 10);
            *data = malloc(sizeof(val));
            *(*(long**)data) = val;
            return INI_TYPE_INTEGER;
        }
        // parse as double
        else {
            double val = strtod(value, NULL);
            *data = malloc(sizeof(val));
            *(*(double**)data) = val;
            return INI_TYPE_FLOAT;
        }
    }
    // error
    else {
        TRACE("Error parsing INI value - invalid data type");
        return INI_TYPE_VOID;
    }
}

int parseValueLine(const char * value, unsigned int length, ini_section_t *current) {
    const char *name = value;
    unsigned int offset = 0;
    // find the end of hte name (whitespace or '=' sign)
    while(name[offset] && !isspace(name[offset]) && name[offset] != '=') ++ offset;
    // if we reached the end of line, that's wrong
    if(!value[offset]) {
        TRACE("Error parsing INI value, unexpected end of line!\n");
        errorID = INI_ERROR_PARSE_ERROR;
        errorstr = "INI value line must be formatted: <name> = <value>";
        return -1;
    }
    unsigned int nameLength = offset;
    // find '=' sign, might already be pointing to it
    while(value[offset] && value[offset] != '=') ++offset;
    if(!value[offset]) {
        TRACE("Error parsing INI value, unexpected end of line!\n");
        errorID = INI_ERROR_PARSE_ERROR;
        errorstr = "INI value line must be formatted: <name> = <value>";
        return -1;
    }
    ++offset;
    // now skip whitespace for value
    while(value[offset] && isspace(value[offset])) ++ offset;
    if(!value[offset]) {
        TRACE("Error parsing INI value, unexpected end of line!\n");
        errorID = INI_ERROR_PARSE_ERROR;
        errorstr = "INI value line must be formatted: <name> = <value>";
        return -1;
    }
    const char * val_ptr = &value[offset];
    void * data = NULL;
    offset = 0;
    int in_str = 0, ignore = 0;
    // if it's a string, keep going until the string terminates, otherwise go until whitespace is found
    for(; val_ptr[offset] && (in_str || !isspace(val_ptr[offset])); ++offset) {
        // a '"' means we're either going into a string, or leaving a string
        if(!ignore) {
            if(val_ptr[offset] == '\\') {
                ignore = 1;
            }
            if(val_ptr[offset] == '"') {
                in_str = !in_str;
                if(!in_str) {
                    offset ++;
                    break;
                }
            }
        }
        else {
            ignore = 0;
        }
    }
    // parse value
    int type = parseValue(val_ptr, offset, &data);
    if(type == INI_TYPE_VOID) {
        TRACE("Error parsing INI value - void value!\n");
        return -1;
    }
    // add value
    if(addValue(current, name, nameLength, data, type) != 0) {
        TRACE("Error adding new INI value!\n");
        return -1;
    }
    // success
    return 0;
}

int parseLine(const char *line, unsigned int length, ini_section_t **current) {
    // deal with comments first
    unsigned int offset = length; // point to last character
    unsigned int speechmark = -1, semicolon = -1;
    speechmark = rfind(line, '"', offset);
    semicolon = rfind(line, ';', offset);
    // to 'remove' the comment we just adjust the length to not include it
    // extra processing is done to ensure that it is actually a comment, and not a semi-colon in a string
    if(speechmark != -1 && semicolon != -1) {
        if(semicolon > speechmark)
            length = semicolon;
        else
            length = speechmark + 1;
    }
    else {
        if(speechmark != -1) length = speechmark + 1;
        if(semicolon != -1)  length = semicolon;
    }
    // empty line, return success
    if(length == 0)
        return 0;
    // we assume that there is no leading or trailing whitespace due to previous processing
    // we also know the line is at least >0 length, because it's start and end offset were different
    if(line[0] == '[') {
        // clearly a section header
        if(parseSectionHeader(line, length, current) != 0) {
            TRACE("Error parsing INI section header!\n");
            return -1; // this okay?
        }
    }
    else {
        if(parseValueLine(line, length, *current) != 0) {
            TRACE("Error parsing INI value line!\n");
            return -1; // okay?
        }
    }
    // success
    return 0;
}

void iniParseLump(const char *inidata) {
    unsigned int start_offset = 0, end_offset = 0;
    // go through the file parsing one line at a time
    ini_section_t *currentSection = NULL;
    while(inidata[end_offset]) {
        // if end of line, have a line parsed and move on
        if(inidata[end_offset] == '\n') {
            // skip leading whitespace
            unsigned end = end_offset;
            while(start_offset < end_offset && isspace(inidata[start_offset])) ++ start_offset;
            while(end_offset > end_offset && isspace(inidata[end_offset])) -- end_offset;
            // don't parse empty lines
            if(start_offset != end_offset) {
                if(parseLine(&inidata[start_offset], end_offset - start_offset, &currentSection) != 0) {
                    // something bad happen - report error to user somehow? :/
                }
            }
            // adjust offsets for next line
            end_offset = end;
            end_offset ++;
            start_offset = end_offset;
        }
        else ++end_offset;
    }
    // if they aren't the same, the file doesn't end with a newline, so parse the last bit
    if(end_offset != start_offset) {
        // skip leading whitespace
        while(start_offset < end_offset && isspace(inidata[start_offset])) ++ start_offset;
        // don't parse empty lines
        if(start_offset != end_offset) {
            if(parseLine(&inidata[start_offset], end_offset - start_offset, &currentSection) != 0) {
                // something bad happen - report error to user somehow? :/
            }
        }
    }
}

int iniGetType(const char * section, const char * value) {
    ini_value_t * found = fetchValueStr(section, value);
    if(!found) return INI_TYPE_VOID;
    return found->type;
}

int iniGetInt(const char *section, const char *value) {
    ini_value_t *found = fetchValueStr(section, value);
    if(!found) return 0;
    if(found->type != INI_TYPE_INTEGER) {
        TRACE("Cannot return integer value for non integer '%s' in section '%s'\n", value, section);
        return 0; // default error value
    }
    return *((int*)found->data);
}

double iniGetFloat(const char *section, const char *value) {
    ini_value_t *found = fetchValueStr(section, value);
    if(!found) return 0.0;
    if(found->type != INI_TYPE_FLOAT) {
        TRACE("Cannot return float value for non float '%s' in section '%s'\n", value, section);
        return 0.0; // default error value
    }
    return *((double*)found->data);
}

int iniGetBool(const char *section, const char *value) {
    ini_value_t *found = fetchValueStr(section, value);
    if(!found) return 0;
    if(found->type != INI_TYPE_BOOLEAN) {
        TRACE("Cannot return boolean value for non boolean '%s' in section '%s'\n", value, section);
        return 0; // default error value
    }
    return *((int*)found->data);
}

const char * iniGetString(const char *section, const char *value) {
    ini_value_t *found = fetchValueStr(section, value);
    if(!found) return NULL;
    if(found->type != INI_TYPE_STRING) {
        TRACE("Cannot return string value for non string '%s' in section '%s'\n", value, section);
        return NULL; // default error value
    }
    return ((const char *)found->data);
}

int iniLastErrorID() {
    return errorID;
}

size_t iniLastErrorSize() {
    if(!errorstr) return 0;
    return strlen(errorstr);
}

void iniLastErrorMessage(char *buffer, size_t count) {
    if(!errorstr) return;
    size_t size = iniLastErrorSize();
    if(count > size) count = size;
    strncpy(buffer, errorstr, count-1);
    buffer[count] = '\0';
}
